#================================================
package MSN3::Notification::SOAP;
#================================================

use strict;
use warnings;

# For Debugging
use Data::Dumper;

# For soap
use LWP::UserAgent;
use SOAP::Lite;

# File handling
use File::Basename;
use MIME::Types qw(by_suffix);

# General
use MIME::Base64;
use HTML::Entities;
use Switch;

sub checksum { my $o = tell(DATA); seek DATA,0,0; local $/; my $t = unpack("%32C*",<DATA>) % 65535;seek DATA,$o,0; return $t};

sub new
{
    my $class = shift;
    my $msn = shift;

    my $self =
    {
        MSN             => $msn,

        UserAgent       => new LWP::UserAgent(
        		timeout         => 5,
        		max_redirect    => 0,
        		agent           => 'MSN Explorer/9.0 (MSN 8.0; TmstmpExt)'
        ),

        TimeStamps      =>
        {
            ABFindContactsPaged => '0001-01-01T00:00:00.00-08:00',
            FindMembership  => '0001-01-01T00:00:00.00-08:00'
        },

        Profile         => {},

        @_
    };
    bless($self, $class);

    return $self;
}

sub DESTROY
{
    my $self = shift;

    # placeholder for possible destructor code
}

sub AUTOLOAD
{
    my $self = shift;

    $self->error( "method $MSN3::Notification::SOAP::AUTOLOAD not defined" ) if( $self->{AutoloadError} );
}

#================================================
# Error / Debugging
#================================================

sub debug
{
    my $self = shift;

    return $self->{MSN}->debug( @_ );
}

sub error
{
    my $self = shift;

    return $self->{MSN}->error( @_ );
}

#================================================
# Initial load
#================================================

sub loadInitial
{
    my $self = shift;

    my $Lists = $self->FindMembership();
    my ($Groups, $Contacts) = $self->ABFindContactsPaged();

    foreach my $contact (values %{ $Contacts })
    {
        if( $contact->{Network} eq "WL" )
        {
            $Lists->{Forward}->{ $contact->{passportName} } = $contact->{contactId};
        }
    }

    $self->{MSN}->getNotification()->{Lists} = $Lists;
    $self->{MSN}->getNotification()->{Groups} = $Groups;
    $self->{MSN}->getNotification()->{Contacts} = $Contacts;

    if(defined $Lists->{Pending})
    {
        foreach my $user (keys %{ $Lists->{Pending} })
        {
            $self->{MSN}->getNotification()->allowContact($user);
        }
    }

    $self->GetProfile();

    return $self->getMembershipXML();
}

sub getMembershipXML
{
    my $self = shift;

    my $Lists = $self->{MSN}->getNotification()->{Lists};

    my $Members = {};
    foreach my $memberlist (keys %{ $Lists })
    {
        foreach my $member (keys %{ $Lists->{ $memberlist } })
        {
            my ($User, $Domain) = split(/\@/, $member, 2);
            if( $memberlist eq "Forward" )
            {
                $Members->{ $Domain }->{ $User } += 1;
            }
            if( $memberlist eq "Allow" )
            {
                $Members->{ $Domain }->{ $User } += 2;
            }
            if( $memberlist eq "Block" )
            {
                $Members->{ $Domain }->{ $User } += 4;
            }
            if( $memberlist eq "Hide" )
            {
                $Members->{ $Domain }->{ $User } += 64;
            }
        }
    }

    my $payload = '<ml>';
    foreach my $domain (keys %{ $Members })
    {
        $payload .= '<d n="'. $domain .'">';
        foreach my $user (keys %{ $Members->{ $domain } })
        {
            if($Members->{ $domain }->{ $user } > 0)
            {
                $payload .= '<c n="'. $user .'" t="1">';
                $payload .= '<s l="'. $Members->{ $domain }->{ $user } .'" n="IM" />';
                $payload .= '<s l="3" n="PE" />';
                $payload .= '<s l="3" n="PF" />';
                $payload .= '</c>';
            }
        }
        $payload .= '</d>';
    }
    $payload .= '</ml>';

    return $payload;
}

#================================================
# SOAP functions
#================================================

sub AddMember
{
    my $self = shift;
    my $handle = shift || return;
    my $role = shift || return;

    my $rolebit = 0;
    switch($role)
    {
        case 'Allow'
        {
            $rolebit = 2;
        }
        case 'Block'
        {
            $rolebit = 4;
        }
        case 'Hide'
        {
            $rolebit = 64;
        }
    }

    if(defined $self->{MSN}->getNotification()->{Lists}->{ $role }->{ $handle })
    {
        my ($domain, $user) = split(/\@/, $handle, 2) if(defined $handle);
        return '<ml><d n="'. $domain .'"><c n="'. $user .'" t="1"><s n="IM" l="'. $rolebit .'"/><s n="PE" l="3"/></c></d></ml>';
    }

    my $ref = $self->callSharing(
        'AddMember',
        'CFE80F9D-180F-4399-82AB-413F33A1FA11',
        'BlockUnblock',
        SOAP::Data->name(
            serviceHandle => \SOAP::Data->value(
                SOAP::Data->name(Id => '0'),
                SOAP::Data->name(Type => 'Messenger'),
                SOAP::Data->name(ForeignId => ''),
            ),
        ),
        SOAP::Data->name(
            memberships => \SOAP::Data->name(
                Membership => \SOAP::Data->value(
                    SOAP::Data->name(MemberRole => $role),
                    SOAP::Data->name(
                        Members => \SOAP::Data->value(
                            SOAP::Data->name(
                                Member => \SOAP::Data->value(
                                    SOAP::Data->name(Type => 'Passport'),
                                    SOAP::Data->name(State => 'Accepted'),
                                    SOAP::Data->name(PassportName => $handle)
                                )
                            )->attr({'xsi:type' => 'PassportMember', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'})
                        )
                    )
                )
            )
        )
    );

    if( defined $ref )
    {
        $self->{MSN}->getNotification()->{Lists}->{ $role }->{ $handle } = 0;
        my ($domain, $user) = split(/\@/, $handle, 2) if(defined $handle);
        return '<ml><d n="'. $domain .'"><c n="'. $user .'" t="1"><s n="IM" l="'. $rolebit .'"/><s n="PE" l="3"/></c></d></ml>';
    }

    return 0;
}

sub DeleteMember
{
    my $self = shift;
    my $handle = shift || return;
    my $role = shift || return;

    my $rolebit = 0;
    switch($role)
    {
        case 'Allow'
        {
            $rolebit = 2;
        }
        case 'Block'
        {
            $rolebit = 4;
        }
        case 'Hide'
        {
            $rolebit = 64;
        }
    }

    if(!defined $self->{MSN}->getNotification()->{Lists}->{ $role }->{ $handle })
    {
        my ($domain, $user) = split(/\@/, $handle, 2) if(defined $handle);
        return '<ml><d n="'. $domain .'"><c n="'. $user .'" t="1"><s n="IM" l="'. $rolebit .'"/><s n="PE" l="3"/></c></d></ml>';
    }

    my $ref = $self->callSharing(
        'DeleteMember',
        'CFE80F9D-180F-4399-82AB-413F33A1FA11',
        'BlockUnblock',
        SOAP::Data->name(
            serviceHandle => \SOAP::Data->value(
                SOAP::Data->name(Id => '0'),
                SOAP::Data->name(Type => 'Messenger'),
                SOAP::Data->name(ForeignId => ''),
            ),
        ),
        SOAP::Data->name(
            memberships => \SOAP::Data->name(
                Membership => \SOAP::Data->value(
                    SOAP::Data->name(MemberRole => $role),
                    SOAP::Data->name(
                        Members => \SOAP::Data->value(
                            SOAP::Data->name(
                                Member => \SOAP::Data->value(
                                    SOAP::Data->name(Type => 'Passport'),
                                    SOAP::Data->name(State => 'Accepted'),
                                    SOAP::Data->name(PassportName => $handle)
                                )
                            )->attr({'xsi:type' => 'PassportMember', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'})
                        )
                    )
                )
            )
        )
    );

    if( defined $ref )
    {
        delete($self->{MSN}->getNotification()->{Lists}->{ $role }->{ $handle });
        my ($domain, $user) = split(/\@/, $handle, 2) if(defined $handle);
        return '<ml><d n="'. $domain .'"><c n="'. $user .'" t="1"><s n="IM" l="'. $rolebit .'"/><s n="PE" l="3"/></c></d></ml>';
    }

    return 0;
}

sub FindMembership
{
    my $self = shift;
    my $lastChanged = shift || $self->{TimeStamps}->{FindMembership};

    my $PartnerScenario = 'Initial';
    if( $lastChanged ne '0001-01-01T00:00:00.00-08:00' )
    {
        $PartnerScenario = 'Timer';
    }

    my $ref = $self->callSharing(
        'FindMembership',
        '3794391A-4816-4BAC-B34B-6EC7FB5046C6',
        $PartnerScenario,
        SOAP::Data->name(
            serviceFilter => \SOAP::Data->name(
                Types => \SOAP::Data->value(
                    SOAP::Data->name(ServiceType => 'Messenger'),
                    SOAP::Data->name(ServiceType => 'IMAvailability')
                )
            ),
        ),
        SOAP::Data->name(View => 'Full'),
        SOAP::Data->name(deltasOnly => 'false'),
        SOAP::Data->name(LastChanged => $lastChanged)
    );

    my $Lists = {};
    my $i = 0;
    foreach my $membership ($ref->dataof('//FindMembershipResult/Services/Service/Memberships/*'))
    {
        $i++;
        $membership = $ref->match("//FindMembershipResult/Services/Service/Memberships/[$i]");
        my $role = $membership->valueof('MemberRole');
        foreach my $member ($membership->valueof('Members/Member'))
        {
            if($member->{'Type'} eq "Passport")
            {
                $Lists->{ $role }->{ $member->{PassportName} } = $member->{CID};
            }
        }
    }

    return $Lists;
}

sub ABFindContactsPaged
{
    my $self = shift;
    my $lastChanged = shift || $self->{TimeStamps}->{ABFindContactsPaged};

    my $PartnerScenario = 'Initial';
    if( $lastChanged ne '0001-01-01T00:00:00.00-08:00' )
    {
        $PartnerScenario = 'Timer';
    }

    my $ref = $self->callAb(
        'ABFindContactsPaged',
        '3794391A-4816-4BAC-B34B-6EC7FB5046C6',
        $PartnerScenario,
        SOAP::Data->name(
            pageContext => \SOAP::Data->value(
                SOAP::Data->name(PageSize => '1500'),
                SOAP::Data->name(Direction => 'Forward')
            ),
        ),
        SOAP::Data->name(
            filterOptions => \SOAP::Data->value(
                SOAP::Data->name(DeltasOnly => 'false'),
                SOAP::Data->name(LastChanged => $lastChanged),
                SOAP::Data->name(
                    ContactFilter => \SOAP::Data->value(
                        SOAP::Data->name(IncludeHiddenContacts => 'true'),
                        SOAP::Data->name(IncludeShellContacts => 'false'),
                    ),
                )
            )
        ),
        SOAP::Data->name(abView => 'MessengerClient8'),
        SOAP::Data->name(extendedContent => 'AB AllGroups CircleResult'),
    );

    $self->{MSN}->{CircleTicket} = encode_base64($ref->valueof('//ABFindContactsPagedResult/CircleResult/CircleTicket'), '');

    my $Groups = {};
    foreach my $group ($ref->match('//ABFindContactsPagedResult/Groups/Group'))
    {
        my $Group = {
            groupGuid => $ref->valueof('groupId'),
            name => $ref->valueof('groupInfo/name'),
        };

        $Groups->{ $Group->{groupGuid} } = $Group;
    }

    if($PartnerScenario eq "Initial")
    {
        $self->{Profile}->{CID} = $ref->valueof("//ABFindContactsPagedResult/Ab/abInfo/OwnerCID");
    }

    my $Contacts = {};
    my $i = 0;
    foreach my $contact ($ref->dataof('//ABFindContactsPagedResult/Contacts/*'))
    {
        $i++;
        $contact = $ref->match("//ABFindContactsPagedResult/Contacts/[$i]");
        my $contactinfo = $contact->valueof('contactInfo');
        my $Contact = {
            contactId       => $contact->valueof('contactInfo/CID'),
            contactGuid     => $contact->valueof('contactId'),
            displayName     => $contactinfo->{displayName},
            hasSpace        => $contactinfo->{hasSpace},
            birthdate       => $contactinfo->{birthdate},

            Network         => '',
            Groups          => {},
            Status          => 'FLN'
        };

        foreach my $network ($contact->valueof('contactInfo/NetworkInfoList/NetworkInfo'))
        {
            switch($network->{SourceId})
            {
                case "WL"
                {
                    $Contact->{Network} = 'WL';
                }
            }
        }

        if(defined $contactinfo->{SourceHandle})
        {
            $Contact->{Network} = $contactinfo->{SourceHandle}->{SourceID};
        }

        foreach my $group ($contact->valueof('contactInfo/groupIds/guid'))
        {
            if(defined $Groups->{$group})
            {
                $Contact->{Groups}->{$group} = $Groups->{$group}->{name};
            }
        }

        switch($Contact->{Network})
        {
            case "WL"
            {
                $Contact->{passportName} = $contactinfo->{passportName};
            }
            case "FB"
            {
                $Contact->{passportName} = $contactinfo->{SourceHandle}->{ObjectID};
            }
            else
            {
                #print Dumper($contactinfo);
            }
        }

        next if(!defined $Contact->{passportName});
        $Contacts->{ $Contact->{contactGuid} } = $Contact;
    }

    return ($Groups, $Contacts);
}

sub GetProfile
{
    my $self = shift;

    my $ref = $self->callStorage(
        'GetProfile',
        'RoamingSeed',
        SOAP::Data->name(
            profileHandle => \SOAP::Data->value(
                SOAP::Data->name(
                    Alias => \SOAP::Data->value(
                        SOAP::Data->name(Name => $self->{Profile}->{CID}),
                        SOAP::Data->name(NameSpace => 'MyCidStuff')
                    )
                ),
                SOAP::Data->name(RelationshipName => 'MyProfile'),
            ),
        ),
        SOAP::Data->name(
            profileAttributes => \SOAP::Data->value(
                SOAP::Data->name(ResourceID => 'true'),
                SOAP::Data->name(DateModified => 'true'),
                SOAP::Data->name(
                    ExpressionProfileAttributes => \SOAP::Data->value(
                        SOAP::Data->name(ResourceID => 'true'),
                        SOAP::Data->name(DateModified => 'true'),
                        SOAP::Data->name(DisplayName => 'true'),
                        SOAP::Data->name(DisplayNameLastModified => 'true'),
                        SOAP::Data->name(PersonalStatus => 'true'),
                        SOAP::Data->name(PersonalStatusLastModified => 'true'),
                        SOAP::Data->name(StaticUserTilePublicURL => 'true'),
                        SOAP::Data->name(Photo => 'true'),
                        SOAP::Data->name(Flags => 'true'),
                    )
                )
            ),
        ),
    );

    $self->{Profile}->{DisplayName} = $ref->valueof("//GetProfileResult/ExpressionProfile/DisplayName");
    $self->{Profile}->{PersonalStatus} = $ref->valueof("//GetProfileResult/ExpressionProfile/PersonalStatus");

    $self->{Profile}->{DisplayPicture} = {};
    if($ref->dataof("//GetProfileResult/ExpressionProfile/Photo"))
    {
        $self->{Profile}->{DisplayPicture} = {
            ResourceID => $ref->valueof("//GetProfileResult/ExpressionProfile/Photo/ResourceID"),
            Type => $ref->valueof("//GetProfileResult/ExpressionProfile/Photo/DocumentStreams/DocumentStream/DocumentStreamType"),
            Size => $ref->valueof("//GetProfileResult/ExpressionProfile/Photo/DocumentStreams/DocumentStream/DataSize"),
            PreUrl => $ref->valueof("//GetProfileResult/ExpressionProfile/Photo/DocumentStreams/DocumentStream/PreAuthURL"),
            Url => $ref->valueof("//GetProfileResult/ExpressionProfile/StaticUserTilePublicURL"),
        };
    }

    return $self->{Profile};
}

sub CreateDocument
{
    my $self = shift;
    my $file = shift || return;
    my $type = shift || return;

    if(!-e $file)
    {
        return $self->error("File does not exist.");
    }

    my $filemime = (by_suffix($file))[0];
    my $filedata = '';
    my $doctype = '';

    switch($filemime)
    {
        case "image/jpeg" { $doctype = "Photo"; }
        case "image/png" { $doctype = "Photo"; }
        case "image/gif" { $doctype = "Photo"; }
        else { $doctype = "File"; }
    }

    my $DocumentType = '';
    my $ParentRelationship = '';
    my $DocumentRelationship = '';

    switch($type)
    {
        case "UserTileStatic" {
            $DocumentType = 'UserTileStatic';
            $ParentRelationship = '/UserTiles';
            $DocumentRelationship = 'Messenger User Tile';

            if($self->{Profile}->{Photo}->{Type} eq $DocumentType)
            {
                return $self->UpdateDocument($file, $type);
            }
        }
    }

    open( DP, $file ); binmode(DP);
    while( <DP> ) { $filedata .= $_; }
    close(DP);

    my $ref = $self->callStorage(
        'CreateDocument',
        'RoamingIdentityChanged',
        SOAP::Data->name(
            parentHandle => \SOAP::Data->value(
                SOAP::Data->name(RelationshipName => $ParentRelationship),
                SOAP::Data->name(
                    Alias => \SOAP::Data->value(
                        SOAP::Data->name(Name => $self->{Profile}->{CID}),
                        SOAP::Data->name(NameSpace => 'MyCidStuff')
                    )
                )
            ),
        ),
        SOAP::Data->name(
            document => \SOAP::Data->value(
                SOAP::Data->name(Name => basename($file)),
                SOAP::Data->name(
                    DocumentStreams => \SOAP::Data->value(
                        SOAP::Data->name(
                            DocumentStream => \SOAP::Data->value(
                                SOAP::Data->name(DocumentStreamType => $DocumentType),
                                SOAP::Data->name(MimeType => $filemime),
                                SOAP::Data->name(Data => $filedata),
                                SOAP::Data->name(DataSize => '0'),
                            )
                        )->attr({'xsi:type' => $doctype .'Stream'})
                    ),
                )
            )
        )->attr({'xsi:type' => $doctype}),
        SOAP::Data->name(relationshipName => $DocumentRelationship),
    );

    return $ref->valueof("");
}

sub callStorage
{
    my $self = shift;

    my $service = shift;
    my $partner = shift || 'Initial';

    my $soap = SOAP::Lite
    -> uri('http://www.msn.com/webservices/storage/w10')
    -> on_action( sub { sprintf '%s/%s', @_ } )
    -> proxy('https://storage.msn.com/storageservice/SchematizedStore.asmx');
    $soap->cache_ttl(1);

    #print Dumper($self->{MSN}->{Tokens});

    my $StorageApplicationHeader = SOAP::Header->name(
        StorageApplicationHeader => \SOAP::Header->value(
            SOAP::Header->name(ApplicationID => 'Messenger Client 8.5'),
            SOAP::Header->name(Scenario => $partner)
        )
    )->uri('http://www.msn.com/webservices/storage/w10')->prefix('');

    my $StorageUserHeader = SOAP::Header->name(
        StorageUserHeader => \SOAP::Header->value(
            SOAP::Header->name(Puid => '0'),
            SOAP::Header->name(TicketToken => $self->{MSN}->{Tokens}->{'storage.msn.com'}->{'Token'})
        )
    )->uri('http://www.msn.com/webservices/storage/w10')->prefix('');

    my $resp = $soap->call(
        $service,
        $StorageApplicationHeader,
        $StorageUserHeader,
        @_
    );

  	if($resp->fault)
  	{
        $self->error('SOAP ERROR: ['.$service.']: '.$resp->faultstring);
        print Dumper($resp); exit;
    }
    else
    {
        return $resp;
    }

  	return 0;
}

sub callSharing
{
    my $self = shift;

    my $service = shift;
    my $appid = shift || '';
    my $partner = shift || 'Initial';

    my $soap = SOAP::Lite
    -> uri('http://www.msn.com/webservices/AddressBook')
    -> on_action( sub { sprintf '%s/%s', @_ } )
    -> proxy('https://contacts.msn.com/abservice/SharingService.asmx');
    $soap->cache_ttl(1);

    my $ABAppHeader = SOAP::Header->name(
        ABApplicationHeader => \SOAP::Header->value(
            SOAP::Header->name(ApplicationId => $appid),
            SOAP::Header->name(IsMigration => 'false'),
            SOAP::Header->name(PartnerScenario => $partner)
        )
    )->uri('http://www.msn.com/webservices/AddressBook')->prefix('');

    my $ABAuthHeader = SOAP::Header->name(
        ABAuthHeader => \SOAP::Header->value(
            SOAP::Header->name(ManagedGroupRequest => 'false'),
            SOAP::Header->name(TicketToken => $self->{MSN}->{Tokens}->{'contacts.msn.com'}->{'Token'})
        )
    )->uri('http://www.msn.com/webservices/AddressBook')->prefix('');

    my $resp = $soap->call(
        $service,
        $ABAppHeader,
        $ABAuthHeader,
        @_
    );

  	if($resp->fault)
  	{
        $self->error('SOAP ERROR: ['.$service.']: '.$resp->faultstring);
    }
    else
    {
        return $resp;
    }

  	return 0;
}

sub callAb
{
    my $self = shift;

    my $service = shift;
    my $appid = shift || '';
    my $partner = shift || 'Initial';

    my $soap = SOAP::Lite
    -> uri('http://www.msn.com/webservices/AddressBook')
    -> on_action( sub { sprintf '%s/%s', @_ } )
    -> proxy('https://contacts.msn.com/abservice/abservice.asmx');
    $soap->cache_ttl(1);

    my $ABAppHeader = SOAP::Header->name(
        ABApplicationHeader => \SOAP::Header->value(
            SOAP::Header->name(ApplicationId => $appid),
            SOAP::Header->name(IsMigration => 'false'),
            SOAP::Header->name(PartnerScenario => $partner)
        )
    )->uri('http://www.msn.com/webservices/AddressBook')->prefix('');

    my $ABAuthHeader = SOAP::Header->name(
        ABAuthHeader => \SOAP::Header->value(
            SOAP::Header->name(ManagedGroupRequest => 'false'),
            SOAP::Header->name(TicketToken => $self->{MSN}->{Tokens}->{'contacts.msn.com'}->{'Token'})
        )
    )->uri('http://www.msn.com/webservices/AddressBook')->prefix('');

    my $resp = $soap->call(
        $service,
        $ABAppHeader,
        $ABAuthHeader,
        @_
    );

  	if($resp->fault)
  	{
        $self->error('SOAP ERROR: ['.$service.']: '.$resp->faultstring);
    }
    else
    {
        return $resp;
    }
}

return 1;
__DATA__